/* eslint-disable */
import {
	MOUSE,
	Matrix4,
	PerspectiveCamera,
	Quaternion,
	Spherical,
	TOUCH,
	Vector2,
	Vector3
} from 'three';

// This set of controls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
//
//    Orbit - left mouse / touch: one-finger move
//    Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
//    Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move

const STATE = {
	NONE: - 1,
	ROTATE: 0,
	DOLLY: 1,
	PAN: 2,
};

const mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };

const twoPI = 2 * Math.PI;

const EPS = 0.000001;

class OrbitControls {
	domElement: any;
	object: any;
	// "target" sets the location of focus, where the object orbits around
	target= new Vector3();
	// How far you can dolly in and out ( PerspectiveCamera only )
	minDistance = 0;
	maxDistance= Infinity;
	// How far you can zoom in and out ( OrthographicCamera only )
	minZoom = 0;
	maxZoom = Infinity;
	// How far you can orbit vertically, upper and lower limits.
	// Range is 0 to Math.PI radians.
	minPolarAngle= 0;
	maxPolarAngle= Math.PI;
	zoomSpeed = 1.0;
	rotateSpeed= 1.0;
	panSpeed= 1.0;
	target0: any;
	position0: any;
	zoom0: any;
	state= STATE.NONE;
	// Set to true to enable damping (inertia)
	// If damping is enabled, you must call controls.update() in your animation loop
	enableDamping= true;
	dampingFactor= 0.05;
	// if false, pan orthogonal to world-space direction camera.up
	screenSpacePanning= true;
	spherical = new Spherical();
	// How far you can orbit horizontally, upper and lower limits.
		// If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
	minAzimuthAngle= - Infinity;
	maxAzimuthAngle= Infinity;
	scale= 1;

	private zoomChanged = false;
	private quat;
	private quatInverse;

	constructor( object: PerspectiveCamera, domElement: Document | HTMLCanvasElement | undefined ) {
		if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' );
		if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' );

		this.object = object;
		this.domElement = domElement;
		this.domElement.style.touchAction = 'none'; // disable touch scroll

		// for reset
		this.target0 = this.target.clone();
		this.position0 = this.object.position.clone();
		this.zoom0 = this.object.zoom;

		this.quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
		this.quatInverse = this.quat.clone().invert();

		this.addEventListener();
		this.update();
	}

	private __offset = new Vector3();
	private __lastPosition = new Vector3();
	private __lastQuaternion = new Quaternion();
	update() {
		const position = this.object.position;
		const spherical = this.spherical;
		this.__offset.copy( position ).sub( this.target );
		// rotate offset to "y-axis-is-up" space
		this.__offset.applyQuaternion( this.quat );
		// angle from z-axis around y-axis
		spherical.setFromVector3( this.__offset );
		if ( this.enableDamping ) {
			spherical.theta += this.sphericalDelta.theta * this.dampingFactor;
			spherical.phi += this.sphericalDelta.phi * this.dampingFactor;
		} else {
			spherical.theta += this.sphericalDelta.theta; 
			spherical.phi += this.sphericalDelta.phi;
		}

		// restrict theta to be between desired limits

		let min = this.minAzimuthAngle;
		let max = this.maxAzimuthAngle;

		if ( isFinite( min ) && isFinite( max ) ) {
			if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;
			if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;
			if ( min <= max ) {
				spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );
			} else {
				spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?
					Math.max( min, spherical.theta ) :
					Math.min( max, spherical.theta );
			}

		}

		// restrict phi to be between desired limits
		spherical.phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, spherical.phi ) );
		spherical.makeSafe();

		spherical.radius *= this.scale;
		// restrict radius to be between desired limits
		spherical.radius = Math.max( this.minDistance, Math.min( this.maxDistance, spherical.radius ) );
		// move target to panned location
		if ( this.enableDamping === true ) {
			this.target.addScaledVector( this.panOffset, this.dampingFactor );
		} else {
			this.target.add( this.panOffset );
		}

		this.__offset.setFromSpherical( spherical );

		// rotate offset back to "camera-up-vector-is-up" space
		this.__offset.applyQuaternion( this.quatInverse );

		position.copy( this.target ).add( this.__offset );

		this.object.lookAt( this.target );

		if ( this.enableDamping === true ) {
			this.sphericalDelta.theta *= ( 1 - this.dampingFactor );
			this.sphericalDelta.phi *= ( 1 - this.dampingFactor );
			this.panOffset.multiplyScalar( 1 - this.dampingFactor );
		} else {
			this.sphericalDelta.set( 0, 0, 0 );
			this.panOffset.set( 0, 0, 0 );
		}

		this.scale = 1;

		// update condition is:
		// min(camera displacement, camera rotation in radians)^2 > EPS
		// using small-angle approximation cos(x/2) = 1 - x^2 / 8

		if ( this.zoomChanged ||
			this.__lastPosition.distanceToSquared( this.object.position ) > EPS ||
			8 * ( 1 - this.__lastQuaternion.dot( this.object.quaternion ) ) > EPS ) {
			this.__lastPosition.copy( this.object.position );
			this.__lastQuaternion.copy( this.object.quaternion );
			this.zoomChanged = false;
			return true;
		}

		return false;
	}

	private sphericalDelta = new Spherical();
	private panOffset = new Vector3();
	private rotateStart = new Vector2();
	private rotateEnd = new Vector2();
	private rotateDelta = new Vector2();

	private panStart = new Vector2();
	private panEnd = new Vector2();
	private panDelta = new Vector2();

	private dollyStart = new Vector2();
	private dollyEnd = new Vector2();
	private dollyDelta = new Vector2();

	rotateLeft( angle: number ) {
		this.sphericalDelta.theta += angle;
	}

	rotateUp( angle: number ) {
		this.sphericalDelta.phi += angle;
	}

	dollyOut( dollyScale: number ) {
		if ( this.object.isPerspectiveCamera ) {
			this.scale /= dollyScale;
		} else if ( this.object.isOrthographicCamera ) {
			this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom * dollyScale ) );
			this.object.updateProjectionMatrix();
			this.zoomChanged = true;
		} else {
			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
		}
	}

	dollyIn( dollyScale: number ) {
		if ( this.object.isPerspectiveCamera ) {
			this.scale *= dollyScale;
		} else if ( this.object.isOrthographicCamera ) {
			this.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / dollyScale ) );
			this.object.updateProjectionMatrix();
			this.zoomChanged = true;
		} else {
			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
		}
	}

	saveState() {
		this.target0.copy( this.target );
		this.position0.copy( this.object.position );
		this.zoom0 = this.object.zoom;
	};

	getPolarAngle() {
		return this.spherical.phi;
	};

	getAzimuthalAngle() {
		return this.spherical.theta;
	};

	getZoomScale() {
		return Math.pow( 0.95, this.zoomSpeed );
	}

	getDistance() {
		return this.object.position.distanceTo( this.target );
	};

	onContextMenu( event: { preventDefault: () => void; } ) {
		event.preventDefault();
	}

	handleMouseWheel( event: any) {
		if ( event.deltaY < 0 ) {
			this.dollyIn(this.getZoomScale());
		} else if ( event.deltaY > 0 ) {
			this.dollyOut(this.getZoomScale());
		}
		this.update();
	}

	onMouseWheel = ( event: { preventDefault?: any; deltaY?: number; } ) => {
		if (this.state !== STATE.NONE ) return;
		event.preventDefault();
		this.handleMouseWheel( event );
	}

	onMouseDown = ( event: {
		clientY: any;
		clientX: any; pointerId?: any; button?: any; ctrlKey?: any; metaKey?: any; shiftKey?: any; 
} ) => {
		let mouseAction;
		switch ( event.button ) {
			case 0:
				mouseAction = mouseButtons.LEFT;
				break;
			case 1:
				mouseAction = mouseButtons.MIDDLE;
				break;
			case 2:
				mouseAction = mouseButtons.RIGHT;
				break;
			default:
				mouseAction = - 1;
		}

		switch ( mouseAction ) {
			case MOUSE.DOLLY:
				this.dollyStart.set( event.clientX, event.clientY );
				this.state = STATE.DOLLY;
				break;
			case MOUSE.ROTATE:
				if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
					this.panStart.set( event.clientX, event.clientY );
					this.state = STATE.PAN;
				} else {
					this.rotateStart.set( event.clientX, event.clientY );
					this.state = STATE.ROTATE;
				}

				break;

			case MOUSE.PAN:
				if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
					this.rotateStart.set( event.clientX, event.clientY );
					this.state = STATE.ROTATE;
				} else {
					this.panStart.set( event.clientX, event.clientY );
					this.state = STATE.PAN;
				}
				break;
			default:
				this.state = STATE.NONE;
		}
	}

	handleMouseMoveRotate( event: { clientX: any; clientY: any; } ) {
		this.rotateEnd.set( event.clientX, event.clientY );
		this.rotateDelta.subVectors( this.rotateEnd, this.rotateStart ).multiplyScalar( this.rotateSpeed );
		const element = this.domElement;
		this.rotateLeft( 2 * Math.PI * this.rotateDelta.x / element.clientHeight ); // yes, height
		this.rotateUp( 2 * Math.PI * this.rotateDelta.y / element.clientHeight );
		this.rotateStart.copy( this.rotateEnd );
		this.update();
	}

	handleMouseMoveDolly( event: { clientX: number; clientY: number; } ) {
		this.dollyEnd.set( event.clientX, event.clientY );
		this.dollyDelta.subVectors( this.dollyEnd, this.dollyStart );
		if (this.dollyDelta.y > 0 ) {
			this.dollyOut( this.getZoomScale() );
		} else if (this.dollyDelta.y < 0 ) {
			this.dollyIn( this.getZoomScale() );
		}
		this.dollyStart.copy( this.dollyEnd );
		this.update();
	}

	handleMouseMovePan( event: { clientX: number; clientY: number; } ) {
		this.panEnd.set( event.clientX, event.clientY );
		this.panDelta.subVectors( this.panEnd, this.panStart ).multiplyScalar( this.panSpeed );
		this.pan( this.panDelta.x, this.panDelta.y );
		this.panStart.copy( this.panEnd );
		this.update();
	}
	private __panOffset = new Vector3();
	// deltaX and deltaY are in pixels; right and down are positive
	pan (deltaX: number, deltaY: number ) {
		const element = this.domElement;
		if ( this.object.isPerspectiveCamera ) {
			// perspective
			const position = this.object.position;
			this.__panOffset.copy( position ).sub( this.target );
			let targetDistance = this.__panOffset.length();
			// half of the fov is center to top of screen
			targetDistance *= Math.tan( ( this.object.fov / 2 ) * Math.PI / 180.0 );
			// we use only clientHeight here so aspect ratio does not distort speed
			this.panLeft( 2 * deltaX * targetDistance / element.clientHeight, this.object.matrix );
			this.panUp( 2 * deltaY * targetDistance / element.clientHeight, this.object.matrix );
		} else if ( this.object.isOrthographicCamera ) {
			// orthographic
			this.panLeft( deltaX * ( this.object.right - this.object.left ) / this.object.zoom / element.clientWidth, this.object.matrix );
			this.panUp( deltaY * ( this.object.top - this.object.bottom ) / this.object.zoom / element.clientHeight, this.object.matrix );
		} else {
			// camera neither orthographic nor perspective
			console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
		}
	};

	private _panLeftV =  new Vector3();
	panLeft (distance: number, objectMatrix: Matrix4) {
		this._panLeftV.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
		this._panLeftV.multiplyScalar( - distance );
		this.panOffset.add( this._panLeftV );
	};

	private _panUpV =  new Vector3();
	panUp (distance: number, objectMatrix: Matrix4) {
		if ( this.screenSpacePanning === true ) {
			this._panUpV.setFromMatrixColumn( objectMatrix, 1 );
		} else {
			this._panUpV.setFromMatrixColumn( objectMatrix, 0 );
			this._panUpV.crossVectors( this.object.up, this._panUpV );
		}

		this._panUpV.multiplyScalar( distance );
		this.panOffset.add( this._panUpV );
	};

	onMouseMove= ( event: { clientX: number; clientY: number; } ) => {
		switch (this.state) {
			case STATE.ROTATE:
				this.handleMouseMoveRotate( event );
				break;

			case STATE.DOLLY:
				this.handleMouseMoveDolly( event );
				break;

			case STATE.PAN:
				this.handleMouseMovePan( event );
				break;

		}
	}

	onPointerUp = ( event: { pointerId: any; } ) => {
		this.domElement.releasePointerCapture( event.pointerId );
		this.domElement.removeEventListener( 'pointermove', this.onMouseMove );
		this.domElement.removeEventListener( 'pointerup', this.onPointerUp );
		this.state = STATE.NONE;
	}

	onPointerDown = ( event: any ) => {
		this.domElement.setPointerCapture( event.pointerId );
		this.domElement.addEventListener( 'pointermove', this.onMouseMove );
		this.domElement.addEventListener( 'pointerup', this.onPointerUp );
		//
		this.onMouseDown( event );
	}

	addEventListener() {
		this.domElement.addEventListener( 'contextmenu', this.onContextMenu );
		this.domElement.addEventListener( 'wheel', this.onMouseWheel, { passive: false } );
		this.domElement.addEventListener( 'pointerdown', this.onPointerDown );
	}

	dispose() {
		this.domElement.removeEventListener( 'contextmenu', this.onContextMenu );
		this.domElement.removeEventListener( 'wheel', this.onMouseWheel );
		this.domElement.removeEventListener( 'pointerdown', this.onPointerDown );
		this.domElement.removeEventListener( 'pointermove', this.onMouseMove );
		this.domElement.removeEventListener( 'pointerup', this.onPointerUp );
	}

	reset () {
		this.target.copy( this.target0 );
		this.object.position.copy( this.position0 );
		this.object.zoom = this.zoom0;
		this.object.updateProjectionMatrix();
		this.update();
		this.state = STATE.NONE;
	};
}


// This set of controls performs orbiting, dollying (zooming), and panning.
// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
// This is very similar to OrbitControls, another set of touch behavior
//
//    Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate
//    Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
//    Pan - left mouse, or arrow keys / touch: one-finger move

class MapControls extends OrbitControls {
	screenSpacePanning: boolean;
	mouseButtons: any;
	touches: any;

	constructor( object: any, domElement: any ) {
		super( object, domElement );
		this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up
		this.mouseButtons.LEFT = MOUSE.PAN;
		this.mouseButtons.RIGHT = MOUSE.ROTATE;
		this.touches.ONE = TOUCH.PAN;
		this.touches.TWO = TOUCH.DOLLY_ROTATE;
	}
}

export { OrbitControls, MapControls };
